<?php
use Pinenut\Model2;
use Pinenut\Model2\Embedded\Reference;
use Pinenut\Model2\Embedded\ReferenceContainer\Many;
use Pinenut\Model2\Embedded\ReferenceContainer\One;
use Pinenut\Model2\Updater;
use Pinenut\Log;

/**
 * Игровая сессия
 *
 * Привязывается к сессии посетителя
 * Обладает состоянием в одном из вариантов:
 * 1) Новая (переход ко второму пункту)
 * 2) Предложен факт
 * 3) Получен ответ на факт (переходит или к новому факту, или к догадке)
 * 4) Предложена догадка (вывод на основании ответов)
 * 5) Получен ответ на догадку (в зависимости от ответа либо угадали и конец игры, либо предложение ввести свой вариант вывода)
 * 6) Конец игры - победа эксперта
 * 7) Предложение ввести/выбрать загаданный вывод
 * 8) Вывод игрока введён
 * 9) Предложение ввести/выбрать факт отличающий догадку от вывода игрока
 * 10) Факт выбран/введён
 * 11) Конец игры - победа игрока
 * 12) Игра прервана
 *
 * @author Kosh
 *        
 */
class Round extends Model2 {
	const STATE_NEW = 0;
	const STATE_QUESTION = 1;
	const STATE_ANSWER = 2;
	const STATE_GUESS = 3;
	const STATE_GUESS_ANSWER = 4;
	const STATE_END_LOSS = 5;
	const STATE_IMAGINED_QUESTION = 6;
	const STATE_IMAGINED_ANSWER = 7;
	const STATE_FACT_QUESTION = 8;
	const STATE_FACT_ANSWER = 9;
	const STATE_END_WIN = 10;
	const STATE_END_CANCEL = 11;
	public $state = self::STATE_NEW;
	public $session; // идентификатор сессии пользователя
	public $time; // время последнего игрового действия
	/**
	 * Ответы на вопросы
	 *
	 * @var AnswerReference[]
	 */
	public $facts;
	
	/**
	 * Догадка
	 *
	 * @var One
	 */
	public $guess;
	
	/**
	 * Вывод в текстовом виде (нужен если не был найден уже готовый)
	 *
	 * @var string
	 */
	public $conclusionTitle;
	
	/**
	 * Пользовательский вывод
	 *
	 * @var One
	 */
	public $conclusion;
	
	/**
	 * Факт (нужен если не был найден уже готовый)
	 *
	 * @var string
	 */
	public $factTitle;
	
	/**
	 * Пользовательский факт отличающий догадку от пользовательского вывода
	 *
	 * @var One
	 */
	public $fact;
	/**
	 * Признак обработанности раунда - было произведено обучение
	 *
	 * @var boolean
	 */
	public $considered = false;
	public $cacheProbabilityKeys = array ();
	public $cacheProbabilityCount = array ();
	public $cacheProbabilityWeight = array ();
	
	function fields() {
		return array_merge ( parent::fields (), [ 
				'state' => self::INT,
				'session' => self::STRING,
				'time' => self::DATE,
				'facts' => [ 
						Many::className (), // название класса - контейнера
						                    // остальные параметры передаются в конструктор контейнера в третий параметр конструктора
						AnswerReference::className (), // название класса вложенного документа (связи в данном случае)
						Fact::className () 
				],
				'guess' => [ 
						One::className (),
						Reference::className (),
						Conclusion::className () 
				],
				'conclusionTitle' => self::STRING,
				'conclusion' => [ 
						One::className (),
						Reference::className (),
						Conclusion::className () 
				],
				'factTitle' => self::STRING,
				'fact' => [ 
						One::className (),
						Reference::className (),
						Fact::className () 
				],
				'considered' => self::BOOLEAN,
				
				'cacheProbabilityKeys' => self::MIXED,
				'cacheProbabilityCount' => self::MIXED,
				'cacheProbabilityWeight' => self::MIXED
		] );
	}
	
	/**
	 * Получаем текущую игру
	 *
	 * @param string $session        	
	 * @return Round null
	 */
	static function getCurrent($session) {
		return static::find ()->where ( 'session', $session )->orderReverse ( 'time' )->active ()->one ();
	}
	
	/**
	 * Возвращаем очередной факт
	 */
	function getFact() {
		$fact = null;
		
		foreach ( $this->facts as $ref ) {
			if (null === $ref->answer) {
				// нашли неуточненный факт
				$fact = $ref->getEntity ();
				break;
			}
		}
		
		// если не нашли неуточненный факт - подбираем другой для уточнения
		if (! $fact) {
			$fact = $this->pickUpFact ();
			// print_r($fact);exit;
			if ($fact) {
				$this->facts [$fact];
				if (static::STATE_NEW == $this->state || static::STATE_ANSWER == $this->state) {
					$this->state = static::STATE_QUESTION;
				}
			} else {
				// не можем выдвинуть факт, нужно выдвинуть догадку если есть
				$this->checkGuess ( true );
				if ($this->state != static::STATE_GUESS) {
					$this->state = static::STATE_IMAGINED_QUESTION;
				}
			}
			
			$this->time = time ();
			$this->save ();
		}
		return $fact;
	}
	function getGuess() {
		$guess = $this->guess->get ();
		if ($guess) {
			// throw new Exception(print_r($guess, true));
			return $guess->getEntity ();
		} else {
			throw new Exception ( 'Не найден догадка! WTF?!' );
		}
	}
	function getConclusion() {
		if ($this->conclusion) {
			$conclusion = $this->conclusion->get ();
			if ($conclusion) {
				// throw new Exception(print_r($guess, true));
				return $conclusion->getEntity ();
			} else {
				throw new Exception ( 'Не найден вывод! WTF?!' );
			}
		}
	}
	
	/**
	 * Подбираем факт по уже имеющимся уточнениям
	 *
	 * Выбираем тот факт, для которого будет максимальная разница между весами
	 *
	 * Алгоритм
	 * 1) получаем веса выводов
	 * 1+)если весов меньше двух (один ответ только дан), получаем 100 случайных выводов с весом 1
	 * и по ним уже будем подбирать факт, максимально их разделяющий
	 * 2) берём наивероятные выводы
	 * 3) для каждого факта вывода считаем вес
	 * 4) находим сумму абсолютных разниц
	 * для каждого веса и ранее посчитанного веса факта
	 * 5) максимальная сумма разницы весов для факта и будет искомый факт
	 */
	function pickUpFact() {
		$size = 10;
		
		$fact = null;
		Log::begin ();
		// факты, которые уже были в игре - их нужно отбрасывать
		
		$roundFacts = [ ];
		foreach ( $this->facts as $f ) {
			$roundFacts [] = ( string ) $f->_ref;
		}
		
		$probability = $this->calculateProbability ( true );
		
		Log::end ( 'pickUpFact-calculateProbability' );
		
		Log::begin ();
		// если недостаточно выводов
		if (count ( $probability ) <= 2) {
			$probability = array_merge ( $probability, array_fill_keys ( $this->getRandomConclusions ( $size - count ( $probability ) ), 1 ) );
		}
		Log::end ( 'pickUpFact-getRandomConclusions' );
		
		if (count ( $probability ) > 1) {
			// если вариантов два и более строим рейтинг вопросов,
			// и находим максимальную разницу в весах для результатов
			Log::begin ();
			// достаём выводы
			$conclusions = Conclusion::find ()->where ( '_id', array_slice ( array_keys ( $probability ), 0, $size ) )->active ( false )->all ();
			Log::end ( 'pickUpFact-find' );
			
			Log::begin ();
			$factWeights = [ ];
			$factSums = [ ];
			if (is_array ( $conclusions ))
				foreach ( $conclusions as $conclusion ) {
					$id = ( string ) $conclusion ['_id'];
					$cw = $probability [$id];
					foreach ( $conclusion ['facts'] as $ref ) {
						// готовим массивы
						$key = ( string ) $ref ['_ref'];
						
						// пропускаем уже отыгранный факт
						if (in_array ( $key, $roundFacts )) {
							continue;
						}
						
						if (! isset ( $factWeights [$key] )) {
							$factWeights [$key] = [ ];
						}
						if (! isset ( $factSums [$key] )) {
							$factSums [$key] = 0;
						}
						
						// для каждого факта вывода
						$weight = (0 == intval ( $ref ['count'] )) ? 0 : (intval ( $ref ['sum'] ) / intval ( $ref ['count'] ));
						
						// бежим по всем ранее посчитанным весам
						
						$sum = 0;
						foreach ( $factWeights [$key] as $w ) {
							$sum += abs ( $w - $weight ) * $cw;
						}
						$factSums [$key] += $sum;
						$factWeights [$key] [] = $weight;
					}
				}
				// ищем максимальную разницу
			$maxSum = 0;
			$maxId = null;
			foreach ( $factSums as $id => $sum ) {
				if ($maxSum < $sum) {
					$maxSum = $sum;
					$maxId = $id;
				}
			}
			Log::end ( 'pickUpFact-calculate' );
			
			if ($maxId) {
				Log::begin ();
				$fact = Fact::find ( $maxId );
				Log::end ( 'pickUpFact-load-fact' );
			}
		}
		
		// если факт не определён - берём любой не предлагавшийся пользователю
		// TODO: переделать на выбор одного из самых "равно-разделяющих"
		if (! $fact) {
			$finder = Fact::find ();
			$count = $finder->count ();
			if (! $count) {
				return null;
			}
			$offset = rand ( 0, $count - 1 );
			
			$fact = $finder->offset ( $offset )->active ()->one ();
			
			if (in_array ( ( string ) $fact->_id, $roundFacts )) {
				$fact = null;
			}
		}
		
		return $fact;
	}
	function getRandomConclusions($size) {
		$ids = [ ];
		$finder = Conclusion::find ();
		$count = $finder->count ();
		
		for($i = 0; $i < 5; $i ++) {
			$offset = $count - ceil ( $size / 5 );
			if ($offset >= 0) {
				$offset = rand ( 0, $offset );
				$conclusions = $finder->offset ( $offset )->limit ( ceil ( $size / 5 ) )->active ( false )->all ();
				foreach ( $conclusions as $row ) {
					$ids [] = ( string ) $row ['_id'];
				}
			}
		}
		return $ids;
	}
	
	/**
	 * Получаем вероятные выводы по игре
	 * Судя по всему придётся считать в PHP, хотелось мапредюс монговский, но он медленный
	 * а агрегирование не получится, т.к.
	 * там есть вычисления (ответы, веса)
	 *
	 * Алгоритм:
	 * 1) Получаем связи с выводами для фактов, на которые есть ответ.
	 * 2) Для каждой связи факта считаем вес и кол-во по следующей формуле:
	 * 1) если ответ нулевой ("не знаю"), то вес стремится к единице при стремящемуся к нулю весу связи
	 * 2) если ответ положительный - просто вес, если отрицательный то -вес
	 * 3) суммируем веса и кол-во учтенных для каждого вывода
	 * 4) по всем весам вывода пробегаемся и корректируем их ценность в обратной зависимости от кол-ва связей
	 * 5) сортируем
	 *
	 * TODO: медленно работает. необходимо продумать кеширование, ведь каждый раз добавляется всего
	 * лишь новый ответ, остальные веса считать уже не нужно
	 *
	 * @param string $positiveOnly
	 *        	включать только позитивные выводы
	 */
	function calculateProbability($positiveOnly = false) {
		Log::begin ();
		
		$answers = $this->getAnswers ();
		
		$keys = $answerKeys = array_keys ( $answers );
		$weights = $counts = [ ];
		// вычисляем какие факты не закешированы
		$keys = array_diff ( $answerKeys, $this->cacheProbabilityKeys );
		$weights = $this->cacheProbabilityWeight;
		$counts = $this->cacheProbabilityCount;
		
		Log::end ( 'cc-prepareKeys' );
		
		if (! empty ( $keys )) {
			Log::begin ();
			$facts = Fact::find ()->where ( '_id', $keys )->active ( false )->all ();
			Log::end ( 'cc-loadFActs' );
			
			Log::begin ();
			// для каждого ответа
			if (is_array ( $facts )) {
				foreach ( $facts as $fact ) {
					$answer = intval ( $answers [( string ) $fact ['_id']] );
					
					// для каждого вывода
					foreach ( ( array ) $fact ['conclusions'] as $weight ) {
						$ref = ( string ) $weight ['_ref'];
						$count = intval ( $weight ['count'] );
						$sum = intval ( $weight ['sum'] );
						
						if (! isset ( $weights [$ref] )) {
							$weights [$ref] = 1;
						}
						
						if (! isset ( $counts [$ref] )) {
							$counts [$ref] = 0;
						}
						
						if (0 == $count) {
							$w = 0;
						} else {
							if (0 == $answer)
								$w = 1 - abs ( $sum / $count );
							else
								$w = ($answer > 0 ? $sum : - $sum) / $count;
						}
						
						// $weights[$ref]['weight'] += $w;
						$weights [$ref] *= ($w + 1.2);
						
						$counts [$ref] ++;
					}
				}
				Log::end ( 'cc-calc1' );
			}
			
			$this->cacheProbabilityWeight = $weights;
			$this->cacheProbabilityCount = $counts;
			$this->cacheProbabilityKeys = $answerKeys;
		}
		
		Log::begin ();
		$probability = [];
		// вычисляем веса
		$count = count ( $weights );
		foreach ( $weights as $key => $value ) {
			// тут предполагается вводить нормализацию от кол-ва отцененых выводов для факта - чем их меньше, тем хуже достоверность веса и его нужно уменьшить...
			// или как-то так, что-то я запутался...
			$div = $count - $counts [$key];
			$div = ($div != 0) ? $div : 1;
			$probability[$key] = $value/$div;
		}
		Log::end ( 'cc-calc2-' . $count );
		
		Log::begin ();
		
		// сортируем по весу
		
		arsort($probability);
		
// 		usort ( $weights, function ($a, $b) {
// 			return $a ['weight'] == $b ['weight'] ? 0 : ($a ['weight'] < $b ['weight'] ? 1 : - 1);
// 		} );
		
		Log::end ( 'cc-sort' );
		
		Log::begin ();
		// Восстанавливаем ключи
		
		$div = 1;
		if (count ( $probability )) {
			$div = reset($probability);
			$div = $div?$div:1;
		}
		// нормализуем
		foreach ( $probability as $key=>&$val ) {
			//if (! $positiveOnly || ($positiveOnly && $val > 0))
//				$weightsSorted [( string ) $val ['_ref']] = (0 == $div) ? $val ['weight'] : $val ['weight'] / $div;
				$val /= $div;
		}
		Log::end ( 'cc-calc3-' . count ( $probability ) );
//		Log::msg ( 'probability---', $probability );
		return $probability;
	}
	function getAnswers() {
		$answers = [ ];
		foreach ( $this->facts as $ref ) {
			// if (null !== $ref->answer) {
			$answers [( string ) $ref] = $ref->answer;
			// }
		}
		return $answers;
	}
	
	/**
	 *
	 * @param boolean $confirm        	
	 * @throws Exception
	 */
	function confirmGuess($confirm) {
		if (static::STATE_GUESS != $this->state) {
			throw new Exception ( 'Состояние игры не соответствует действию подтверждения догадки' );
		}
		
		if ($confirm) {
			$this->state = static::STATE_END_LOSS;
		} else {
			$this->state = static::STATE_IMAGINED_QUESTION;
		}
		$this->time = time ();
		$this->save ();
		
		if (static::STATE_END_LOSS == $this->state) {
			$this->consider ();
		}
	}
	
	/**
	 * Начинаем новую игру, не забываем закрыть старые
	 *
	 * @param unknown $sessionId        	
	 */
	static function begin($sessionId) {
		
		// отменяем незаконченные игры
		static::update ()->whereNot ( 'state', [ 
				static::STATE_END_CANCEL,
				static::STATE_END_LOSS,
				static::STATE_END_WIN,
				static::STATE_NEW 
		] )->where ( 'session', $sessionId )->set ( 'state', static::STATE_END_CANCEL )->execute ();
		
		// если есть новая игра - хорошо
		$round = static::find ()->where ( 'session', $sessionId )->where ( 'state', static::STATE_NEW )->one ();
		
		if (! $round) {
			// иначе создаём новую игру
			$round = new static ();
			$round->session = $sessionId;
			$round->state = static::STATE_NEW;
			$round->time = time ();
			$round->save ();
		}
		
		return $round;
	}
	
	/**
	 * Даём ответ на факт
	 *
	 * @param unknown $sessionId        	
	 * @param unknown $id        	
	 * @param unknown $answer        	
	 */
	function answer($id, $answer) {
		if (static::STATE_QUESTION != $this->state) {
			throw new Exception ( 'Состояние игры не соответствует действию вопроса' );
		}
		
		foreach ( $this->facts as $ref ) {
			if ($ref->_ref == $id) {
				if (null !== $ref->answer) {
					throw new Exception ( 'Ответ уже определён' );
				}
				$ref->answer = $answer;
				$this->state = static::STATE_ANSWER;
				$this->time = time ();
				$this->save ();
				return;
			}
		}
		throw new Exception ( 'Не найден факт, на который даётся ответ' );
	}
	
	/**
	 * Выдвигаем догадку если это можно
	 */
	function checkGuess($any = false) {
		if (static::STATE_ANSWER !== $this->state && static::STATE_NEW !== $this->state) {
			throw new Exception ( 'Состояние игры не соответствует действию проверки догадки' );
		}
		
		$weights = $this->calculateProbability ( true );
		$guess = null;
		
		if (count ( $weights )) {
			
			// сравниваем сумму весов с первым элементом,
			// если первый элемент больше суммы - значит нашли
			reset($weights);
			list ( $leadKey, $lead ) = each ( $weights );
			
			$sum = array_sum ( array_slice ( $weights, 0, 10 ) );
//			Log::msg('guess!!! ' . $sum . ' - ' . $lead);
			if (($sum - $lead < 1) || $any) {
				$guess = $leadKey;
			}
		} elseif ($any) {
			$guess = Conclusion::find ()->one ();
		}
		
		// если нашли догадку - сохраняем её
		if ($guess) {
			$this->guess [$guess];
			$this->state = static::STATE_GUESS;
			$this->time = time ();
			$this->save ();
		}
	}
	
	/**
	 * Сохраняем загаданный вывод
	 *
	 * @param string $title        	
	 * @throws Exception
	 */
	function imaginedConclusion($title) {
		if (static::STATE_IMAGINED_QUESTION != $this->state) {
			throw new Exception ( 'Состояние игры не соответствует действию' );
		}
		
		if ('' == trim ( $title )) {
			throw new Exception ( 'Вы ничего не загадали?' );
		}
		
		// ищем введённый вывод, может уже есть
		$conclusion = Conclusion::find ()->where ( 'title', $title )->active ( false )->one ();
		
		if ($conclusion) {
			// если нашли вывод, прописываем его в раунд
			$this->conclusion [$conclusion ['_id']];
		}
		
		$this->conclusionTitle = $title;
		
		// если у нас пустая база то догадки может и не быть, то гда сразу валим в спасибо
		if ($this->guess->isEmpty ()) {
			$this->state = static::STATE_END_WIN;
		} else {
			$this->state = static::STATE_FACT_QUESTION;
		}
		$this->save ();
		
		// обучаем
		if ($this->guess->isEmpty ()) {
			$this->consider ();
		}
	}
	
	/**
	 * Сохраняем загаданный факт
	 *
	 * @param string $title        	
	 * @throws Exception
	 */
	function imaginedFact($title) {
		if (static::STATE_FACT_QUESTION != $this->state) {
			throw new Exception ( 'Состояние игры не соответствует действию' );
		}
		
		if ('' == trim ( $title )) {
			throw new Exception ( 'Вы ничего не загадали?' );
		}
		
		// ищем введённый факт, может уже есть
		$fact = Fact::find ()->where ( 'title', $title )->active ( false )->one ();
		
		if ($fact) {
			// если нашли вывод, прописываем его в раунд
			$this->fact [$fact ['_id']];
		}
		
		$this->factTitle = $title;
		$this->state = static::STATE_END_WIN;
		
		$this->save ();
		
		$this->consider ();
	}
	/**
	 * Заводим если нужно новый вывод и факт
	 * Расписываем веса
	 */
	function consider() {
		if (static::STATE_END_LOSS != $this->state && static::STATE_END_WIN != $this->state) {
			throw new Exception ( 'Состояние игры не соответствует действию обучения' );
		}
		
		// если игрок проиграл а мы угадали - просто укрепляем связи
		// if (static::STATE_END_LOSS == $this->state) {
		// $this->teach ();
		// }
		// если игрок выиграл нужно создать новый факт и вывод, плюсануть и минусануть догадку с фактом
		// и проинвертировав обновить связи
		if (static::STATE_END_WIN == $this->state) {
			
			if ($this->conclusion->isEmpty () && ! empty ( $this->conclusionTitle )) {
				// ищем - может кто уже создал
				$conclusion = Conclusion::find ()->where ( 'title', $this->conclusionTitle )->active ( false )->one ();
				if ($conclusion) {
					$this->conclusion [( string ) $conclusion ['_ref']];
				} else {
					// создаём новый вывод
					$conclusion = new Conclusion ();
					$conclusion->title = $this->conclusionTitle;
					$conclusion->save ();
					$this->conclusion [$conclusion];
				}
			}
			
			if ($this->fact->isEmpty () && ! empty ( $this->factTitle )) {
				// ищем - может кто уже создал
				$fact = Fact::find ()->where ( 'title', $this->factTitle )->active ( false )->one ();
				if ($fact) {
					$this->fact [( string ) $fact ['_ref']];
				} else {
					// создаём новый вывод
					$fact = new Fact ();
					$fact->title = $this->factTitle;
					$fact->save ();
					$this->fact [$fact];
				}
			}
		}
		
		$this->teach ();
		
		$this->considered = true;
		
		$this->save ();
	}
	
	/**
	 * Обучаем систему по ответам раунда
	 *
	 * А именно - в зависимости от ответа обновляем веса связей фактов ответов к указанноу выводу
	 *
	 * @param Reference $conclusion        	
	 */
	function teach($conclusion) {
		$conclusionPlus = null;
		
		if (static::STATE_END_WIN == $this->state) {
			// если победил игрок - плюсуем его вариат
			// и минусуем догадку относительно введённого
			// пользователем факта
			
			if (! $this->fact->isEmpty ()) {
				$fact = new Fact ( $this->fact->get () );
				
				if (! $this->guess->isEmpty ()) {
					$conclusion = new Conclusion ( $this->guess->get () );
					$fact->conclusions [$conclusion]->inc ( 'sum', - 1 )->inc ( 'count', 1 );
					$conclusion->facts [$fact]->inc ( 'sum', - 1 )->inc ( 'count', 1 );
					$conclusion->save ();
				}
				if (! $this->conclusion->isEmpty ()) {
					$conclusion = new Conclusion ( $this->conclusion->get () );
					$fact->conclusions [$conclusion]->inc ( 'sum', + 1 )->inc ( 'count', 1 );
					$conclusion->facts [$fact]->inc ( 'sum', + 1 )->inc ( 'count', 1 );
					$conclusion->save ();
					// плюсуем все ответы относительно варианта игрока
					$conclusionPlus = $conclusion;
				}
				
				$fact->save ();
			}
		}
		if (static::STATE_END_LOSS == $this->state) {
			// плюсуем факты для догадки
			if (! $this->guess->isEmpty ()) {
				$conclusionPlus = new Conclusion ( $this->guess->get () );
			}
		}
		
		// плюсуем
		if ($conclusionPlus) {
			foreach ( $this->facts as $answer ) {
				$fact = new Fact ( $answer );
				$fact->conclusions [$conclusionPlus]->inc ( 'sum', $answer->answer )->inc ( 'count', 1 );
				$fact->save ();
				$conclusionPlus->facts [$fact]->inc ( 'sum', $answer->answer )->inc ( 'count', 1 );
			}
			$conclusionPlus->save ();
		}
	}
}